package com.ehsunbehravesh.mypasswords.io;

import com.ehsunbehravesh.mypasswords.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Observable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class FileEncryptor extends Observable implements Runnable {
  
  // <editor-fold defaultstate="collapsed" desc="CONSTANTS">
  public static final int MODE_ENCRYPTION = 1;
  public static final int MODE_DECRYPTION = 2;
  // </editor-fold>
  
  // <editor-fold defaultstate="collapsed" desc="FIELDS">
  private int mode;
  private int bufferSize;
  private byte[] key;
  private File sourceFile;
  private File destinationFile;
  private Long sizeOfSourceFile;
  private Long sizeOfDestinationFile;
  // </editor-fold>

  // <editor-fold defaultstate="collapsed" desc="CONSTRUCTORS">
  public FileEncryptor(int mode, byte[] key, File sourceFile, File destinationFile, int bufferSize) {
    this.mode = mode;
    this.key = key;
    this.bufferSize = (int) (mode == MODE_ENCRYPTION ? bufferSize : (Math.floor(bufferSize / 16) + 1) * 16);
    this.sourceFile = sourceFile;
    this.destinationFile = destinationFile;

    sizeOfSourceFile = sourceFile.length();
  }
  // </editor-fold>

  // <editor-fold defaultstate="collapsed" desc="METHODS">  
  public void run() {
    FileInputStream fileInputStream = null;
    FileOutputStream fileOutputStream = null;
    boolean noError = true;
    try {
      try {
        fileInputStream = new FileInputStream(sourceFile);        
        try {
          fileOutputStream = new FileOutputStream(destinationFile);

          byte[] buffer = new byte[bufferSize];
          byte[] realReadData = null;
          byte[] writingData = null;
          int readBytes = -1;
          try {
            while ((readBytes = fileInputStream.read(buffer)) != -1) {
              realReadData = new byte[readBytes];
              System.arraycopy(buffer, 0, realReadData, 0, realReadData.length);
              buffer = new byte[bufferSize];

              if (mode == MODE_ENCRYPTION) {
                try {
                  writingData = encrypt(realReadData);
                } catch (Exception ex) {
                  noError = false;
                  setChanged();
                  notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION_IN_ENCRYPTION, ex.getMessage()));
                }
              } else if (mode == MODE_DECRYPTION) {
                try {
                  writingData = deccrypt(realReadData);
                } catch (Exception ex) {
                  noError = false;
                  setChanged();
                  notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION_IN_DECRYPTION, ex.getMessage()));
                }
              }

              try {
                fileOutputStream.write(writingData);
              } catch (IOException ex) {
                noError = false;
                setChanged();
                notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION_IN_WRITING_FILE, ex.getMessage()));
              }
              setChanged();
              notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_PROGRESS));
              try {
                Thread.sleep(1);
              } catch (InterruptedException ex) {
                Logger.log(ex.getMessage());
              }
            }
          } catch (IOException ex) {
            noError = false;
            setChanged();
            notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION_IN_READING_FILE, ex.getMessage()));
          }
        } catch (FileNotFoundException ex) {
          noError = false;
          setChanged();
          notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION_IN_WRITING_FILE, ex.getMessage()));
        }
      } catch (FileNotFoundException ex) {
        noError = false;
        setChanged();
        notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION_IN_READING_FILE, ex.getMessage()));
      }
    } catch (Exception ex) {
      if (noError) {
        noError = false;
        setChanged();
        notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_EXCEPTION, ex.getMessage()));
      }
    } finally {
      if (fileInputStream != null) {
        try {
          fileInputStream.close();
        } catch (IOException ex) {
          Logger.log(ex.getMessage());
        }
      }
      if (fileOutputStream != null) {
        try {
          fileOutputStream.close();
        } catch (IOException ex) {
          Logger.log(ex.getMessage());
        }
      }
    }
    if (noError) {
      sizeOfDestinationFile = destinationFile.length();
      setChanged();
      notifyObservers(new FileEncryptorMessage(FileEncryptorMessage.MESSAGE_FINISHED));
    }
  }

  private byte[] encrypt(byte[] data) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    Cipher c = Cipher.getInstance("AES");
    SecretKeySpec k = new SecretKeySpec(key, "AES");
    c.init(Cipher.ENCRYPT_MODE, k);
    byte[] byteEncrypted = c.doFinal(data);
    return byteEncrypted;
  }

  private byte[] deccrypt(byte[] data) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    Cipher c = Cipher.getInstance("AES");
    SecretKeySpec k = new SecretKeySpec(key, "AES");
    c.init(Cipher.DECRYPT_MODE, k);
    byte[] byteText = c.doFinal(data);

    return byteText;
  }
  // </editor-fold>

  // <editor-fold defaultstate="collapsed" desc="PROPERTIES">
  public Long getSizeOfDestinationFile() {
    return sizeOfDestinationFile;
  }

  public Long getSizeOfSourceFile() {
    return sizeOfSourceFile;
  }
  // </editor-fold>
}
